macOS allows you to directly distribute a Network Extension using Developer ID signing, but with an important wrinkle. This post explains that wrinkle, its affect on Xcode, and how you get around it.
If you have questions or comments, start a new thread here on the forums. Put it in the App & System Services > Networking and tag it with Network Extension. That way I’ll be sure to see it go by.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Exporting a Developer ID Network Extension
macOS supports a variety of Network Extension (NE) provider types. Starting with macOS 10.15, it’s possible to distribute an app containing NE providers directly, using Developer ID signing. See TN3134 Network Extension provider deployment for the full list of supported provider types.
For your NE provider to work when distributed directly, it must:
Be packaged as a system extension.
Use Developer ID specific entitlements
This post is focused on that second point, because it’s a common source of confusion.
Note If you’re currently shipping an app extension and you want to move to a system extension, see Network Extension Provider Packaging.
This post assumes that you’re building your app with Xcode; if you’re building your app outside of Xcode, you’ll have to adapt these steps to your build system.
Entitlement Matters
A Network Extension system extension and its container app must be signed with the Network Extension entitlement (com.apple.developer.networking.networkextension). That entitlement is an array, with a variety of different element values based on the provider type. For example, a standard NE content filter provider must include the content-filter-provider value.
There are two groups of these values: the standard ones and the ones with the -systemextension suffix. During development and for App Store distribution, use the appropriate standard value. For direct distribution using Developer ID, use the corresponding value with the -systemextension suffix. For example, a Developer ID signed NE content filter must use content-filter-provider-systemextension instead of content-filter-provider.
Xcode Issues
IMPORTANT Xcode 27.0b1 is reported to have fixed this issue, meaning that it should now be possible to export a Developer ID signed app with an Network Extension system extension from the Xcode organiser. I did some basic tests of that here in my office and it seems to work. Yay! So the following is only relevant if you have to build your app with an earlier version of Xcode.
Xcode 26 and earlier are not aware of this requirement. If you build your NE provider container app using Xcode, you might expect to export it for direct distribution using the Direct Distribution workflow in the Xcode organiser. This does not work on older versions of Xcode (r. 108838909).
To get around this, manually export your app from your Xcode archive. Before attempting that, there are a few things to confirm:
By default Xcode’s Signing & Capabilities editor uses the standard values for the NE entitlement. Leave them that way. During day-to-day development it’s best to use an Apple Development signing identity [1], and the standard values work with that.
Continue to use Build > Archive [2] to create an Xcode archive for your product. The steps below replace the Direct Distribution workflow, and they assume you’re starting with an Xcode archive.
Note For hints and tips about how to bring up and then debug an NE provider, see Debugging a Network Extension Provider.
[1] Don’t use Developer ID for day-to-day development; see The Care and Feeding of Developer ID for more on that topic.
[2] Or, if you’re automating this, the archive action in xcodebuild.
Assemble Your Assets
Imagine you’re working on a content filter for the Mac called WaffleFilter. You’ve used Xcode to build the app into an Xcode archive:
% ls "WaffleFilter.xcarchive/Products/Applications"
WaffleFilter.app
That app is development signed:
% codesign -d -vvv "WaffleFilter.xcarchive/Products/Applications/WaffleFilter.app"
…
Authority=Apple Development: …
…
IMPORTANT The steps in this section are based on the much more comprehensive instructions in Creating distribution-signed code for macOS. If anything is unclear, read that documentation for clarification.
To re-sign this app for direct distribution you’ll need three things:
A Developer ID application signing identity. This is named Developer ID Application: TTT, where TTT identifies your team.
A Developer ID provisioning profile for the app. In this example I’ve called this WaffleFilter_Dev_ID.provisionprofile.
A Developer ID provisioning profile for the system extension. In this example I’ve named this WaffleFilter_WFProvider_DevID.provisionprofile.
If you’re not sure how to create these things, see Developer Account Help.
Re-sign the App
To start, make a copy of the app:
% ditto "WaffleFilter.xcarchive/Products/Applications/WaffleFilter.app" "WaffleFilter.app"
Dump the entitlements of the app and its embedded system extension:
% codesign -d --entitlements "WaffleFilter.entitlements" --xml "WaffleFilter.app"
% codesign -d --entitlements "WaffleFilter_WFProvider.entitlements" --xml "WaffleFilter.app/Contents/Library/SystemExtensions/com.example.apple-samplecode.WaffleFilter.WFProvider.systemextension"
And reformat them to make them more readable:
% plutil -convert xml1 "WaffleFilter.entitlements"
% plutil -convert xml1 "WaffleFilter_WFProvider.entitlements"
Now edit these files to add the -systemextension suffix. The result will look something like this:
% cat "WaffleFilter.entitlements"
…
<dict>
…
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>content-filter-provider-systemextension</string>
</array>
…
</dict>
</plist>
% cat "WaffleFilter_WFProvider.entitlements"
…
<dict>
…
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>content-filter-provider-systemextension</string>
</array>
…
</dict>
</plist>
Before you re-sign with these entitlements, replace the embedded provisioning profiles with their Developer ID variants:
% cp "WaffleFilter_Dev_ID.provisionprofile" "WaffleFilter.app/Contents/embedded.provisionprofile"
% cp "WaffleFilter_WFProvider_DevID.provisionprofile" "WaffleFilter.app/Contents/Library/SystemExtensions/com.example.apple-samplecode.WaffleFilter.WFProvider.systemextension/Contents/embedded.provisionprofile"
Now re-sign the app and the system extension with their new entitlements, from the inside out:
% codesign -s "Developer ID Application" -f --entitlements "WaffleFilter_WFProvider.entitlements" --timestamp -o runtime "WaffleFilter.app/Contents/Library/SystemExtensions/com.example.apple-samplecode.WaffleFilter.WFProvider.systemextension"
WaffleFilter.app/Contents/Library/SystemExtensions/com.example.apple-samplecode.WaffleFilter.WFProvider.systemextension: replacing existing signature
% codesign -s "Developer ID Application" -f --entitlements "WaffleFilter.entitlements" --timestamp -o runtime "WaffleFilter.app"
WaffleFilter.app: replacing existing signature
If you have multiple Developer ID Application signing identities, you’ll need to replace Developer ID Application with the name of the specific identity you want to use.
IMPORTANT If your app contains other code items, like frameworks or an app extension, re-sign those as well. For advice on how to manually re-sign a more complex app, see Creating distribution-signed code for macOS.
And you’re done!
Manually Notarise
Xcode’s Direct Distribution workflow also deals with notarisation. As you’re not using that workflow, manually notarise your app. For advice on how to do that, see Customizing the notarization workflow.
You should also look at Packaging Mac Software for Distribution, which has a bunch of general info about packaging Mac apps.
Revision History
2026-06-22 Xcode 27.0b1 is reported to have fixed this issue. Added information about that. Made other minor editorial changes.
2023-09-21 First posted.